Skip to content

Java 集合框架

Java 集合框架 — 系统学习笔记


一、概述

Java 集合框架(Java Collections Framework, JCF)java.util 包中提供的一套统一的数据结构接口与实现,用于存储和操作对象集合。它解决了数组长度固定、类型不安全、缺乏丰富操作等痛点,是 Java 开发中使用最频繁的 API 之一。

核心价值:提供可复用的数据结构和算法,让开发者专注于业务逻辑而非底层数据组织。

集合框架的接口继承层次

mermaid
graph TD
    Iterable --> Collection
    Collection --> List
    Collection --> Set
    Collection --> Queue
    Queue --> Deque
    Set --> SortedSet
    SortedSet --> NavigableSet

    Map --> SortedMap
    SortedMap --> NavigableMap

    List -.->|实现| ArrayList
    List -.->|实现| LinkedList
    List -.->|实现| CopyOnWriteArrayList
    Set -.->|实现| HashSet
    Set -.->|实现| LinkedHashSet
    NavigableSet -.->|实现| TreeSet
    Deque -.->|实现| ArrayDeque
    Queue -.->|实现| PriorityQueue

    Map -.->|实现| HashMap
    Map -.->|实现| LinkedHashMap
    Map -.->|实现| ConcurrentHashMap
    NavigableMap -.->|实现| TreeMap

关键区分Collection 是所有单列集合的根接口;Map键值对集合的根接口,二者是平行关系,Map 并不继承 Collection。


二、核心概念与原理

2.1 Collection 接口

Collection 接口定义了对集合进行基本操作的方法(addremovecontainssizeisEmptyiterator 等),所有实现类必须提供:

  • 无参构造函数:创建空集合
  • 带 Collection 参数的构造函数:用已有集合创建新集合

易混淆点Collection 是接口,Collections 是工具类(提供排序、查找、同步包装等静态方法)。

2.2 Iterator 与快速失败机制

Iterator 是遍历集合的统一方式,核心方法:hasNext()next()remove()

快速失败(fail-fast)

  • ArrayListHashMap 等非并发集合内部维护 modCount 计数器
  • 迭代过程中检测到 modCount 与期望值不一致时,立即抛出 ConcurrentModificationException
  • 目的:尽早暴露并发修改错误,避免数据不一致

安全失败(fail-safe)

  • CopyOnWriteArrayListConcurrentHashMap 等并发集合
  • 遍历的是快照或不抛异常,但可能看到旧数据(弱一致性)

遍历时正确删除元素的方式

java
// ✅ 方式一:使用 Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if ("target".equals(it.next())) {
        it.remove(); // 正确维护 modCount
    }
}

// ✅ 方式二:Java 8+ removeIf
list.removeIf(item -> "target".equals(item));

// ❌ 错误方式:for-each 中直接调用 list.remove()
for (String item : list) {
    if ("target".equals(item)) {
        list.remove(item); // 触发 ConcurrentModificationException
    }
}

2.3 Iterable 与 for-each 语法糖

  • 实现 Iterable 接口的类可使用 for-each,编译器会将其展开为 Iterator 调用
  • 数组for-each 编译为下标访问(无 Iterator 开销)
  • 自定义数据结构实现 Iterable 可无缝融入 Stream、Collections 工具类等生态

2.4 泛型与通配符

通配符语义读写特性示例
?无界只能读为 ObjectList<?>
? extends T上界只读(Producer)List<? extends Number>
? super T下界只写(Consumer)List<? super Integer>

PECS 原则Producer Extends, Consumer Super。从集合取数据用 extends,往集合写数据用 super

类型擦除:Java 泛型在编译后擦除类型信息,运行时 List<String>List<Integer> 均为 List


三、List:有序可重复集合

3.1 ArrayList

3.1.1 底层结构与扩容机制

底层Object[] 动态数组。

扩容流程(JDK 8):

mermaid
flowchart TD
    A["调用 add(e)"] --> B{"table == null ?"}
    B -->|是| C["首次初始化,容量 = 10"]
    B -->|否| D{"size + 1 > capacity ?"}
    D -->|否| E["直接赋值 elementData[size++] = e"]
    D -->|是| F["调用 grow()"]
    F --> G["newCapacity = oldCapacity + oldCapacity >> 1(1.5倍)"]
    G --> H["Arrays.copyOf 复制到新数组"]
    H --> E

关键源码

java
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 新容量 = 旧容量 * 1.5(位运算高效)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

最佳实践:已知元素数量时,使用 new ArrayList<>(expectedSize) 预分配容量,避免频繁扩容(每次扩容是 $O(n)$ 的数组拷贝)。

3.1.2 时间复杂度

操作时间复杂度说明
get(i) / set(i, v)$O(1)$数组下标直接寻址
add(e)(尾部)均摊 $O(1)$偶发扩容
add(i, e)(中间)$O(n)$System.arraycopy 移位
remove(i)$O(n)$后续元素前移
contains(v)$O(n)$线性扫描

3.1.3 subList 的视图陷阱

java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sub = list.subList(1, 3); // [B, C],共享底层数组

sub.set(0, "X"); // list 变为 [A, X, C, D]
list.add("E");   // 结构修改后,再访问 sub 将抛 ConcurrentModificationException

// ✅ 需要独立副本时:
List<String> safeSub = new ArrayList<>(list.subList(1, 3));

3.2 LinkedList

底层双向链表(每个 Node 含 prev/next 指针),同时实现 ListDeque 接口。

操作时间复杂度说明
头尾插删$O(1)$直接操作 head/tail 指针
中间插删$O(1)$(定位后)但定位需 $O(n)$ 遍历
随机访问 get(i)$O(n)$需遍历链表

实际性能:在大多数场景下 LinkedList 性能不如 ArrayList,原因是链表节点内存不连续,CPU 缓存不友好,且插删前需 $O(n)$ 定位。

作为栈/队列:LinkedList 实现了 Deque 接口,可用作双端队列、栈、队列。但 Java 官方文档明确建议:

  • :优先用 ArrayDeque(数组实现,无锁,性能更好)
  • 队列:优先用 ArrayDequeLinkedBlockingQueue(并发场景)
  • 不推荐 Stack:继承 Vector,同步开销大

3.3 CopyOnWriteArrayList

写时复制:每次写操作(add/set/remove)都复制整个底层数组(加 ReentrantLock 锁),写完后原子性替换引用;读操作完全无锁

java
// 适用场景:读多写极少(如监听器列表、配置项)
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("listener1");

// 迭代过程中修改不会抛 ConcurrentModificationException
for (String listener : listeners) {
    listeners.add("newListener"); // 操作的是副本,迭代器看到的是旧快照
}
优点缺点
读无锁,并发读性能极高写操作内存开销大(整体复制)
迭代器不抛 CME数据弱一致性(读到旧版本)
天然线程安全写频繁场景 GC 压力剧增

3.4 ArrayList vs LinkedList 对比

维度ArrayListLinkedList
底层结构动态数组双向链表
随机访问$O(1)$,快$O(n)$,慢
尾部增删均摊 $O(1)$$O(1)$
中间增删$O(n)$(数组拷贝)$O(n)$(遍历定位)
内存占用紧凑(连续内存)每个节点额外存储两个指针
CPU 缓存友好不友好
线程安全

结论:绝大多数场景优先选择 ArrayList


四、Set:无序不重复集合

4.1 HashSet

底层HashMap(key 存元素,value 存固定的 PRESENT 对象)。

元素唯一性保证hashCode() → 定位桶 → equals() → 判断是否重复。

⚠️ 关键:放入 HashSet 的对象必须正确重写 hashCode()equals(),否则会出现"逻辑重复"元素。

java
// 错误示例:未重写 hashCode 和 equals
class User {
    String name;
    // 两个 name 相同的 User 会被当作不同元素存入 HashSet
}

链表转红黑树的阈值机制

当同一个桶的链表长度 $\geq 8$(TREEIFY_THRESHOLD)且数组总容量 $\geq 64$(MIN_TREEIFY_CAPACITY)时,链表转为红黑树,查找复杂度从 $O(n)$ 降至 $O(\log n)$。当红黑树节点数 $\leq 6$(UNTREEIFY_THRESHOLD)时退化回链表。

为何选红黑树而非 AVL 树? 红黑树的旋转次数上限是常数(插入 $\leq 2$ 次,删除 $\leq 3$ 次),而 AVL 树为 $O(\log n)$ 次。在 HashMap 插删频繁的场景下,红黑树的写性能更优

4.2 TreeSet

  • 底层:TreeMap(红黑树),元素按自然顺序或指定 Comparator 排序
  • 增删查均为 $O(\log n)$
  • 独特能力:范围查询(headSet/tailSet/subSet
  • 元素必须实现 Comparable 或构造时传入 Comparator

4.3 LinkedHashSet

  • 底层:LinkedHashMap,在 HashSet 基础上维护双向链表
  • 遍历时按插入顺序输出,查找仍 $O(1)$
  • 内存开销略高于 HashSet

五、Queue 与 Deque

5.1 Queue 接口

代表 FIFO(先进先出)队列:

操作抛异常返回特殊值
插入add(e)offer(e)
移除remove()poll()
检查element()peek()

5.2 ArrayDeque

  • 底层:循环动态数组,可自动扩容
  • 既可作队列(FIFO),也可作(LIFO)
  • 不允许 null 元素
  • 性能优于 LinkedListStack
java
// 作为栈使用
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 压栈
stack.push(2);
stack.pop();   // 弹栈,返回 2
stack.peek();  // 查看栈顶,返回 1

// 作为队列使用
Deque<String> queue = new ArrayDeque<>();
queue.offer("A"); // 入队
queue.offer("B");
queue.poll();      // 出队,返回 A

5.3 PriorityQueue

  • 底层:二叉堆(Binary Heap)
  • 元素按自然顺序或 Comparator 排序,队首始终是最小元素
  • 访问最小元素 $O(1)$,插入/移除 $O(\log n)$
  • 迭代器不保证有序遍历
  • 适用场景:任务调度、Top-K 问题

5.4 阻塞队列(BlockingQueue / BlockingDeque)

属于 java.util.concurrent 包,用于生产者-消费者模式:

实现类底层有界/无界特点
ArrayBlockingQueue数组有界公平/非公平锁可选
LinkedBlockingQueue链表可选有界吞吐量通常高于 Array 版
PriorityBlockingQueue无界支持优先级
LinkedBlockingDeque链表可选有界双端阻塞队列
java
BlockingQueue<String> queue = new LinkedBlockingQueue<>(100);

// 生产者
queue.put("task");       // 队列满时阻塞

// 消费者
String task = queue.take(); // 队列空时阻塞

六、Map:键值对映射

6.1 HashMap 底层结构与原理(JDK 8)

6.1.1 数据结构

数组 + 链表 + 红黑树

Node[] table (默认16)
    ├── [0] → null
    ├── [1] → Node → Node → null  (链表)
    ├── [2] → TreeNode(红黑树根) (链表长度≥8 且 数组≥64 时转换)
    ├── ...
    └── [15] → Node → null

6.1.2 put 流程

mermaid
flowchart TD
    A["put(key, value)"] --> B["hash = (h = key.hashCode()) ^ (h >>> 16)"]
    B --> C{"table == null ?"}
    C -->|是| D["resize() 初始化,容量16"]
    C -->|否| E["i = (n-1) & hash 定位桶"]
    D --> E
    E --> F{"tab[i] == null ?"}
    F -->|是| G["直接放入新 Node"]
    F -->|否| H{"key 相同?(hash相等 && equals)"}
    H -->|是| I["覆盖 value,返回旧值"]
    H -->|否| J{"是 TreeNode?"}
    J -->|是| K["红黑树插入"]
    J -->|否| L["遍历链表尾部插入"]
    L --> M{"链表长度 ≥ 8?"}
    M -->|是| N{"数组长度 ≥ 64?"}
    N -->|是| O["链表转红黑树"]
    N -->|否| P["resize() 扩容"]
    M -->|否| Q["插入完成"]
    G --> R{"++size > threshold?"}
    K --> R
    O --> R
    Q --> R
    R -->|是| S["resize() 扩容"]
    R -->|否| T["返回 null"]

6.1.3 哈希扰动函数

java
static final int hash(Object key) {
    int h;
    // 高16位与低16位异或,使高位也参与桶位置计算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

目的:桶位置只用低位 bits((n-1) & hash),扰动后让高位也参与,减少哈希碰撞

6.1.4 扩容机制(resize)

  • 触发条件:$size > threshold$($= capacity \times loadFactor = 16 \times 0.75 = 12$)
  • 扩容规则:容量翻倍($capacity \ll 1$)
  • 元素重定位:由于容量是 2 的幂,节点新位置要么在原下标,要么在原下标 + 旧容量处(由 hash & oldCap 的结果决定),无需全量重新计算 hash
java
// 扩容时元素重定位的核心逻辑
if ((e.hash & oldCap) == 0)
    // 留在原位置
else
    // 移动到 原位置 + oldCap

最佳实践:已知元素数量时,初始容量设为 expectedSize / 0.75 + 1,避免频繁扩容。

6.1.5 为何数组长度必须是 2 的幂?

  1. 计算索引高效(n - 1) & hash 等价于 hash % n,但位运算更快
  2. 扩容时重定位高效:只需判断 hash & oldCap 的一个 bit 即可

6.2 ConcurrentHashMap

JDK 7 vs JDK 8 实现对比

维度JDK 7JDK 8
锁机制分段锁(Segment 数组)CAS + synchronized 节点锁
锁粒度每段一把锁(默认16段)单个桶(Node)一把锁
数据结构数组 + 链表数组 + 链表 + 红黑树
并发度最多16线程理论上无上限

JDK 8 实现细节

java
// put 操作
// 1. 桶为空 → CAS 无锁写入
// 2. 桶非空 → 对头节点加 synchronized 锁,再操作链表/红黑树

// get 操作 → 完全无锁(Node.val 和 next 用 volatile 修饰)

// size() → 通过分散的 CounterCell 累加(类似 LongAdder)

弱一致性读:迭代器和 size() 不提供实时快照,但不抛 ConcurrentModificationException。这是用弱一致性换取高并发吞吐量的设计权衡。

6.3 LinkedHashMap 与 LRU 缓存

LinkedHashMap 在 HashMap 基础上维护双向链表,支持两种顺序:

  • 插入顺序(默认)
  • 访问顺序accessOrder=true,每次 get/put 将节点移到链表尾部)

手写 LRU 缓存(面试高频):

java
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        // accessOrder=true:按访问顺序排列
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 当元素数量超过容量时,自动移除最久未访问的元素(链表头部)
        return size() > capacity;
    }
}

// 使用
LRUCache<String, Integer> cache = new LRUCache<>(3);
cache.put("a", 1);
cache.put("b", 2);
cache.put("c", 3);
cache.get("a");       // a 移到尾部
cache.put("d", 4);    // 触发移除最久未访问的 b
System.out.println(cache); // {c=3, a=1, d=4}

6.4 TreeMap

  • 底层:红黑树,key 按自然顺序或 Comparator 排序
  • 所有操作 $O(\log n)$
  • 独特 API:firstKey()/lastKey()headMap()/tailMap()/subMap()floorKey()/ceilingKey()
  • 适用场景:需要按 key 排序遍历或范围查询

6.5 Map 实现类对比

集合线程安全底层null keynull value默认容量扩容
HashMap数组+链表/红黑树是(1个)162倍
LinkedHashMapHashMap+双向链表是(1个)162倍
TreeMap红黑树--
Hashtable数组+链表112倍+1
ConcurrentHashMap数组+链表/红黑树162倍

七、底层数据结构速览

7.1 数组

  • 连续内存空间,支持 $O(1)$ 随机访问
  • 寻址公式:$a[i] = baseAddress + i \times dataTypeSize$
  • 索引从 0 开始可以减少一次减法运算(偏移量即下标)
  • 插入/删除平均 $O(n)$,头尾最优 $O(1)$

7.2 链表

类型查询头部操作尾部操作给定节点操作
单向链表头 $O(1)$,其他 $O(n)$$O(1)$$O(n)$-
双向链表头尾 $O(1)$,其他 $O(n)$$O(1)$$O(1)$$O(1)$

7.3 散列表(Hash Table)

  • 通过散列函数将 key 映射为数组下标
  • 散列冲突解决
    • 拉链法(链表/红黑树挂在桶上)— HashMap 使用此方式
    • 开放寻址法(线性探测、二次探测等)

将链表改造为红黑树还有一个重要原因:防止 HashDoS 攻击。攻击者构造大量相同 hash 的 key,使链表退化为 $O(n)$;红黑树将其限制在 $O(\log n)$。

7.4 红黑树

红黑树是自平衡二叉搜索树,五大性质:

  1. 节点是红色或黑色
  2. 根节点是黑色
  3. 叶子节点(NIL)是黑色
  4. 红色节点的子节点必须是黑色(不能有连续红节点)
  5. 从任一节点到叶子的所有路径包含相同数量的黑色节点

时间复杂度:查找、插入、删除均为 $O(\log n)$。


八、集合工具类与最佳实践

8.1 Collections 工具类

java
// 排序(TimSort,稳定排序)
Collections.sort(list);
Collections.sort(list, Comparator.reverseOrder());

// 二分查找(需预排序)
int index = Collections.binarySearch(list, target);

// 不可变视图(修改抛 UnsupportedOperationException)
List<String> unmodifiable = Collections.unmodifiableList(list);

// 同步包装(粗粒度锁,迭代仍需手动同步,不推荐)
List<String> syncList = Collections.synchronizedList(list);

// 其他
Collections.shuffle(list);   // 随机打乱
Collections.frequency(list, element); // 元素出现次数

8.2 Java 9+ 不可变集合工厂方法

java
// 创建真正不可变集合(不允许 null,修改抛 UnsupportedOperationException)
List<Integer> list = List.of(1, 2, 3);
Set<String> set = Set.of("a", "b", "c");
Map<String, Integer> map = Map.of("k1", 1, "k2", 2);

// 超过10个键值对
Map<String, Integer> bigMap = Map.ofEntries(
    Map.entry("k1", 1),
    Map.entry("k2", 2)
    // ...
);

// Java 10+ 创建现有集合的不可变副本
List<String> copy = List.copyOf(mutableList);

不可变集合天然线程安全,推荐在常量、函数参数边界处使用。

8.3 集合选型决策树

mermaid
flowchart TD
    START["需要存储什么?"] --> KV{"键值对?"}
    KV -->|是| SORT_MAP{"需要排序?"}
    SORT_MAP -->|是| TREEMAP["TreeMap"]
    SORT_MAP -->|否| ORDER_MAP{"需要插入/访问顺序?"}
    ORDER_MAP -->|是| LINKEDHASHMAP["LinkedHashMap"]
    ORDER_MAP -->|否| CONC_MAP{"高并发?"}
    CONC_MAP -->|是| CONCURRENTHASHMAP["ConcurrentHashMap"]
    CONC_MAP -->|否| HASHMAP["HashMap"]

    KV -->|否| DUP{"允许重复?"}
    DUP -->|否| SORT_SET{"需要排序?"}
    SORT_SET -->|是| TREESET["TreeSet"]
    SORT_SET -->|否| ORDER_SET{"需要插入顺序?"}
    ORDER_SET -->|是| LINKEDHASHSET["LinkedHashSet"]
    ORDER_SET -->|否| HASHSET["HashSet"]

    DUP -->|是| FIFO{"需要 FIFO/LIFO?"}
    FIFO -->|是| ARRAYDEQUE["ArrayDeque"]
    FIFO -->|否| ACCESS{"随机访问多?"}
    ACCESS -->|是| ARRAYLIST["ArrayList"]
    ACCESS -->|否| LINKEDLIST["LinkedList(慎用)"]

总则:单线程优先 ArrayList/HashMap,多线程用并发集合,不要用 Collections.synchronized*(粗粒度锁性能差)。

8.4 Comparable vs Comparator

维度ComparableComparator
java.langjava.util
方法compareTo(T o)compare(T o1, T o2)
修改类需要修改排序类本身不需要,外部定义
排序策略单一(自然排序)多种(可定义多个 Comparator)
绑定方式静态绑定动态绑定
java
// Comparator 多种排序策略
Comparator<Employee> byName = Comparator.comparing(Employee::getName);
Comparator<Employee> byAge = Comparator.comparingInt(Employee::getAge);
Comparator<Employee> byDeptThenName = Comparator
    .comparing(Employee::getDepartment)
    .thenComparing(Employee::getName);

Collections.sort(employees, byDeptThenName);

九、面试高频问题

Q1:HashMap 的实现原理?JDK 7 与 JDK 8 的区别?

  • 底层使用数组 + 链表 + 红黑树(JDK 8+),数组是主体,链表/红黑树解决哈希冲突
  • JDK 7:数组 + 链表,头插法,多线程下可能出现死循环(resize 时链表成环)
  • JDK 8:数组 + 链表 + 红黑树,尾插法,链表长度 $\geq 8$ 且数组 $\geq 64$ 时转红黑树,修复了死循环但仍线程不安全

Q2:HashMap 的 put 流程?

  1. 判断 table 是否为空,空则 resize 初始化
  2. 计算 key 的 hash(扰动函数),通过 (n-1) & hash 定位桶
  3. 桶为空 → 直接放入新节点
  4. 桶非空 → 判断首节点 key 是否相同 → 相同则覆盖 value
  5. 不同 → 判断是否为 TreeNode → 是则红黑树插入
  6. 否则遍历链表尾插 → 插入后检查链表长度是否 $\geq 8$ → 转红黑树
  7. 插入后 ++size > threshold 则扩容

Q3:HashMap 的扩容机制?

  • 首次 add 初始化容量 16,后续 size > capacity × 0.75 时扩容
  • 容量翻倍($capacity \ll 1$)
  • 扩容后元素重新定位:hash & oldCap == 0 留原位,否则移到 原位 + oldCap
  • 扩容是 $O(n)$ 操作,建议提前设置合理初始容量

Q4:ArrayList 和 LinkedList 的区别?

维度ArrayListLinkedList
底层动态数组双向链表
随机访问$O(1)$$O(n)$
尾部增删均摊 $O(1)$$O(1)$
中间增删$O(n)$$O(n)$(遍历定位)
内存紧凑每节点额外两指针
缓存友好

结论:绝大多数场景优先 ArrayList。

Q5:ConcurrentHashMap 如何保证线程安全?

  • JDK 7:分段锁(Segment),每段独立 ReentrantLock,最多 16 线程并发
  • JDK 8:取消分段锁,改用 CAS + synchronized
    • 桶为空 → CAS 无锁写入
    • 桶非空 → 对头节点加 synchronized
    • get 完全无锁(volatile 保证可见性)
    • size 通过 CounterCell 分散累加(类似 LongAdder)

十、总结

类别推荐实现核心特征
有序可重复ArrayList动态数组,随机访问 $O(1)$,扩容 1.5 倍
频繁头尾操作ArrayDeque循环数组双端队列,替代 Stack/LinkedList
去重无序HashSet底层 HashMap,$O(1)$ 查找
去重有序TreeSet红黑树,$O(\log n)$,支持范围查询
键值对HashMap数组+链表+红黑树,$O(1)$ 均摊
有序键值对TreeMap红黑树,按 key 排序
并发 MapConcurrentHashMapCAS+synchronized,高并发首选
并发 ListCopyOnWriteArrayList写时复制,读多写少场景
LRU 缓存LinkedHashMapaccessOrder=true + removeEldestEntry
优先级队列PriorityQueue二叉堆,队首最小元素
不可变集合List.of() / Map.of()Java 9+,天然线程安全

核心原则

  1. 选对集合类型是性能优化的第一步
  2. 理解底层数据结构(数组、链表、红黑树、哈希表)是写出正确代码的基础
  3. 单线程用标准集合,多线程用 java.util.concurrent 下的并发集合
  4. 避免使用 VectorStackHashtableCollections.synchronized* 等过时方案